package com.gomoku;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.Message;
import android.text.TextUtils;
import android.view.View;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;

// 游戏
class Game extends Socket implements Callback, Runnable {
    // 棋子类型
    public enum Piece {
        NOTHING,    // 什么也没有
        BLACK,      // 黑棋
        WHITE       // 白棋
    }

    // 状态
    public enum Status {
        END,            // 结束
        PLAYING,        // 自己下棋中
        RIVAL_PLAYING   // 对手下棋中
    }

    /**
     * 发送数据的线程。
     */
    private class SendThread extends Thread {
        private final GomokuProtocol mSendData; // 要发送的数据

        /**
         * 构造方法。
         * @param sendData  要发送的数据
         */
        public SendThread(GomokuProtocol sendData) {
            mSendData = sendData;
        }

        /**
         * 线程方法。
         */
        @Override
        public void run() {
            try {
                mSender.writeObject(mSendData);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private final Canvas mDrawer;           // 绘图者
    private final View mGameBoard;          // 游戏区域
    private final Handler mGameCallback;    // 游戏回调
    private boolean mIsBlack;               // 黑棋则为 true，白棋则为 false
    private final Point mLastPos;           // 上一次放置棋子的位置
    private final Paint mPaint;             // 绘图相关信息
    private final int mPieceRadius;         // 棋子半径
    private Piece[][] mPieces;              // 所有棋子
    private String mRivalName;              // 对手的名字
    private ObjectOutputStream mSender;     // 数据发送者
    private Status mStatus;                 // 状态
    private final Handler mUICallback;      // UI 回调
    private String mUsername;               // 用户名

    /**
     * 构造方法。
     * @param callback      回调
     * @param drawer        绘图者
     * @param gameBoard     游戏区域
     */
    public Game(Callback callback, Canvas drawer, View gameBoard) {
        // 初始化成员
        mDrawer = drawer;
        mGameBoard = gameBoard;
        mGameCallback = new Handler(this);
        mIsBlack = true;
        mLastPos = new Point();
        mPaint = new Paint();
        mPieceRadius = Const.GRID_SIZE * 4 / 10;
        mStatus = Status.END;
        mUICallback = new Handler(callback);

        // 重绘游戏区域
        this.drawGameBoard();

        // 开启接收数据的线程
        new Thread(this).start();
    }

    /**
     * 接受或拒绝挑战。
     * @param rivalName 对手的名字
     * @param accept    接受挑战则为 true，否则为 false
     */
    public void acceptInvite(String rivalName, boolean accept) {
        // 如果接受则游戏开始
        if (accept) {
            mRivalName = rivalName;
            this.start(false);
        }

        // 发送结果
        final InviteResult result = new InviteResult(accept, mUsername, rivalName);
        this.sendData(result);
    }

    /**
     * 检查横向。
     * @return 胜利则返回 true，否则返回 false
     */
    private boolean checkHorizontal() {
        final Piece checkPiece = mIsBlack ? Piece.BLACK: Piece.WHITE;  // 要检查的棋子
        int count = 0;                                                  // 连续的棋子的数量

        // 从左到右检查一行
        for (int i = 0; i < Const.GAME_BOARD_POINT_COUNT_H; i++) {
            // 查找棋子
            if (mPieces[mLastPos.y][i] == checkPiece) {
                count++;
            } else {
                count = 0;
            }

            // 如果5个棋子连在一起就胜利
            if (count == Const.WIN_NUMBER) {
                return true;
            }
        }

        return false;
    }

    /**
     * 检查斜向（从左上到右下）。
     * @return 胜利则返回 true，否则返回 false
     */
    private boolean checkOblique1() {
        final Piece checkPiece = mIsBlack ? Piece.BLACK: Piece.WHITE;  // 要检查的棋子
        int count = 0;                                                  // 连续的棋子的数量
        final int min = Math.min(mLastPos.x, mLastPos.y);               // 范围坐标，用以确定左边界和上边界
        final int left = mLastPos.x - min;                              // 左边界
        final int top = mLastPos.y - min;                               // 上边界

        // 从左上到右下检查一斜行
        for (int i = left, j = top; i < Const.GAME_BOARD_POINT_COUNT_H && j < Const.GAME_BOARD_POINT_COUNT_V; i++, j++) {
            // 查找棋子
            if (mPieces[j][i] == checkPiece) {
                count++;
            } else {
                count = 0;
            }

            // 如果5个棋子连在一起就胜利
            if (count == Const.WIN_NUMBER) {
                return true;
            }
        }

        return false;
    }

    /**
     * 检查斜向（从左下到右上）。
     * @return 胜利则返回 true，否则返回 false
     */
    private boolean checkOblique2() {
        final Piece checkPiece = mIsBlack ? Piece.BLACK: Piece.WHITE;  // 要检查的棋子
        int count = 0;                                                  // 连续的棋子的数量
        int left = 0;                                                   // 左边界
        int bottom;                                                     // 下边界

        // 计算左边界和下边界
        if (mLastPos.x + mLastPos.y < Const.GAME_BOARD_POINT_COUNT_H) {
            bottom = mLastPos.x + mLastPos.y;
        } else {
            left = (mLastPos.x + mLastPos.y) - Const.GAME_BOARD_POINT_COUNT_H + 1;
            bottom = Const.GAME_BOARD_POINT_COUNT_V - 1;
        }

        // 从左下到右上检查一斜行
        for (int i = left, j = bottom; i < Const.GAME_BOARD_POINT_COUNT_H && j >= 0; i++, j--) {
            // 查找棋子
            if (mPieces[j][i] == checkPiece) {
                count++;
            } else {
                count = 0;
            }

            // 如果5个棋子连在一起就胜利
            if (count == Const.WIN_NUMBER) {
                return true;
            }
        }

        return false;
    }

    /**
     * 检查纵向。
     * @return 胜利则返回 true，否则返回 false
     */
    private boolean checkVertical() {
        final Piece checkPiece = mIsBlack ? Piece.BLACK: Piece.WHITE;  // 要检查的棋子
        int count = 0;                                                  // 连续的棋子的数量

        // 从上到下检查一列
        for (int j = 0; j < Const.GAME_BOARD_POINT_COUNT_V; j++) {
            // 查找棋子
            if (mPieces[j][mLastPos.x] == checkPiece) {
                count++;
            } else {
                count = 0;
            }

            // 如果5个棋子连在一起就胜利
            if (count == Const.WIN_NUMBER) {
                return true;
            }
        }

        return false;
    }

    /**
     * 关闭游戏。
     */
    @Override
    public void close() {
        // 发送退出游戏请求
        final QuitGame quitGame = new QuitGame(mUsername);
        this.sendData(quitGame);

        try {
            // 延迟一下，防止数据还没发送完成就关闭网络
            Thread.sleep(100);

            // 关闭网络
            super.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 认输。
     */
    public void defeat() {
        // 发送游戏结束数据
        final GameOver gameOver = new GameOver(GameOverReason.SELF_ADMIT, mRivalName);
        this.sendData(gameOver);

        // 游戏结束
        this.gameOver(GameOverReason.SELF_ADMIT);
    }

    /**
     * 画游戏区域。
     */
    private void drawGameBoard() {
        // 画背景色
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(Const.GAME_BOARD_COLOR);
        mDrawer.drawRect(0, 0, mGameBoard.getWidth(), mGameBoard.getHeight(), mPaint);

        // 画线
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Const.GAME_BOARD_BORDER_COLOR);
        for (int i = 1; i <= Const.GAME_BOARD_POINT_COUNT_H; i++) {
            // 画横线
            mDrawer.drawLine(Const.GRID_SIZE, i * Const.GRID_SIZE, mGameBoard.getWidth() - Const.GRID_SIZE, i * Const.GRID_SIZE, mPaint);

            // 画竖线
            mDrawer.drawLine(i * Const.GRID_SIZE, Const.GRID_SIZE, i * Const.GRID_SIZE, mGameBoard.getHeight() - Const.GRID_SIZE, mPaint);
        }

        // 通知游戏区域重绘
        mGameBoard.invalidate();
    }

    /**
     * 寻找其它用户。
     */
    public void findUsers() {
        if (TextUtils.isEmpty(mUsername)) {
            return;
        }

        final FindUsersRequest request = new FindUsersRequest(mUsername);
        this.sendData(request);
    }

    /**
     * 游戏结束。
     * @param reason    结束原因
     */
    private void gameOver(GameOverReason reason) {
        // 将状态设置为已结束
        mStatus = Status.END;

        // 发送通知
        final Message message = Message.obtain();
        message.what = Const.UM_GAME_OVER;
        message.arg1 = reason.ordinal();
        mUICallback.sendMessage(message);
    }

    /**
     * 处理寻找其它用户结果。
     * @param result    结果
     */
    private void handleFindUsers(FindUsersResult result) {
        // 发送通知
        final Message message = Message.obtain();
        message.what = Const.UM_GET_FIND_USERS_RESULT;
        message.obj = result;
        mUICallback.sendMessage(message);
    }

    /**
     * 处理游戏结束。
     * @param gameOver  游戏结束信息
     */
    private void handleGameOver(GameOver gameOver) {
        // 如果没在下棋中，则直接返回
        if (mStatus == Status.END) {
            return;
        }

        // 将状态设置为已结束
        mStatus = Status.END;

        // 发送通知
        GameOverReason reason = gameOver.mReason;
        if (reason == GameOverReason.WON) {
            reason = GameOverReason.LOST;
        }
        else if (reason == GameOverReason.SELF_ADMIT) {
            reason = GameOverReason.RIVAL_ADMIT;
        }
        final Message message = Message.obtain();
        message.what = Const.UM_GAME_OVER;
        message.arg1 = reason.ordinal();
        mUICallback.sendMessage(message);
    }

    /**
     * 处理挑战请求。
     * @param request 请求
     */
    private void handleInviteRequest(InviteRequest request) {
        // 发送通知
        final Message message = Message.obtain();
        message.what = Const.UM_GET_INVITE_REQUEST;
        message.obj = request.mRivalName;
        mUICallback.sendMessage(message);
    }

    /**
     * 处理挑战结果。
     * @param result  结果
     */
    private void handleInviteResult(InviteResult result) {
        // 如果接受则游戏开始
        if (result.mAccept) {
            mRivalName = result.mRivalName;
            this.start(true);
        }

        // 发送通知
        final Message message = Message.obtain();
        message.what = Const.UM_GET_INVITE_RESULT;
        message.arg1 = result.mAccept ? 1: 0;
        message.obj = result.mRivalName;
        mUICallback.sendMessage(message);
    }

    /**
     * 处理登录结果。
     * @param result    结果
     */
    private void handleLogon(LogonResult result) {
        final boolean succeeded = !TextUtils.isEmpty(result.mUsername);
        if (succeeded) {
            // 保存用户名
            mUsername = result.mUsername;
        }

        // 发送通知
        final Message message = Message.obtain();
        message.what = Const.UM_GET_LOGON_RESULT;
        message.obj = succeeded;
        mUICallback.sendMessage(message);
    }

    /**
     * 自定义事件的响应方法。
     * @param message   事件
     * @return 事件在此被处理则返回 true，否则返回 false
     */
    @Override
    public boolean handleMessage(Message message) {
        if (message.what == Const.UM_PUT_PIECE) {
            // 放一个与自己颜色相反的棋子
            this.putPiece(!mIsBlack, message.arg1, message.arg2);

            // 将状态设置为游戏中
            mStatus = Status.PLAYING;
            return true;
        }
        return false;
    }

    /**
     * 处理放置棋子。
     * @param putPiece  棋子信息
     */
    private void handlePutPiece(PutPiece putPiece) {
        final Message message = Message.obtain();
        message.what = Const.UM_PUT_PIECE;
        message.arg1 = putPiece.mX;
        message.arg2 = putPiece.mY;
        mGameCallback.sendMessage(message);
    }

    /**
     * 处理对手退出游戏事件。
     * @param quitGame  退出游戏信息
     */
    private void handleQuitGame(QuitGame quitGame) {
        // 如果不是当前对手，则直接返回
        if (!quitGame.mUsername.equals(mRivalName)) {
            return;
        }

        // 发送通知
        final Message message = Message.obtain();
        message.what = Const.UM_RIVAL_QUIT_GAME;
        message.arg1 = mStatus != Status.END ? 1: 0;
        message.obj = quitGame.mUsername;
        mUICallback.sendMessage(message);

        // 对手名置空
        mRivalName = "";

        // 将状态设置为已结束
        mStatus = Status.END;
    }

    /**
     * 发起挑战。
     * @param rivalName 要挑战的用户的名字
     */
    public void invite(String rivalName) {
        if (TextUtils.isEmpty(rivalName)) {
            return;
        }

        // 发送挑战请求
        final InviteRequest request = new InviteRequest(mUsername, rivalName);
        this.sendData(request);
    }

    /**
     * 判断是否胜利。
     * @return 胜利则返回 true，否则返回 false
     */
    private boolean isWon() {
        // 检查横向
        boolean result = this.checkHorizontal();
        if (result) {
            return true;
        }

        // 检查纵向
        result = this.checkVertical();
        if (result) {
            return true;
        }

        // 检查斜向（从左上到右下）
        result = this.checkOblique1();
        if (result) {
            return true;
        }

        // 检查斜向（从左下到右上）
        return this.checkOblique2();
    }

    /**
     * 登录。
     * @param username  用户名
     */
    public void logon(String username) {
        // 发送登录请求
        final LogonRequest request = new LogonRequest(username);
        this.sendData(request);
    }

    /**
     * 游戏区域的单击响应方法。
     * @param x, y  用户单击的位置
     */
    public void onGameBoardClicked(int x, int y) {
        final int bound = Const.GRID_SIZE / 2;  // 格的尺寸的一半

        // 如果不是己方下棋中（对手下棋中或游戏结束）或者 单击的范围超出棋盘，则直接返回
        if (mStatus != Status.PLAYING ||
            x < bound || x > mGameBoard.getWidth() - bound ||
            y < bound || y > mGameBoard.getHeight() - bound) {
            return;
        }

        // 将用户单击的点转换为离得最近的棋盘点
        int tempX = x;
        int tempY = y;
        int temp = x % Const.GRID_SIZE;
        if (temp >= bound) {
            tempX += Const.GRID_SIZE - temp;
        } else {
            tempX -= temp;
        }

        temp = y % Const.GRID_SIZE;
        if (temp >= bound) {
            tempY += Const.GRID_SIZE - temp;
        } else {
            tempY -= temp;
        }

        // 将窗口坐标转换成棋盘坐标
        tempX = tempX / Const.GRID_SIZE - 1;
        tempY = tempY / Const.GRID_SIZE - 1;

        // 放置棋子
        final boolean put = this.putPiece(mIsBlack, tempX, tempY);
        if (!put) {
            return;
        }

        // 判断是否胜利
        final boolean won = this.isWon();

        // 将已放置的棋子发给对手
        final PutPiece putPiece = new PutPiece(mRivalName, tempX, tempY);
        this.sendData(putPiece);

        // 如果胜利则通知对手，否则等待对手下棋
        if (won) {
            // 发送游戏结束数据
            final GameOver gameOver = new GameOver(GameOverReason.WON, mRivalName);
            this.sendData(gameOver);

            // 游戏结束
            this.gameOver(GameOverReason.WON);
        } else {
            mStatus = Status.RIVAL_PLAYING;
        }
    }

    /**
     * 处理服务器发来的数据。
     * @param pro   协议
     */
    private void onGetMessageFromServer(GomokuProtocol pro) {
        switch (pro.mType) {
            case FIND_USERS:
                this.handleFindUsers((FindUsersResult)pro);
                break;
            case GAME_OVER:
                this.handleGameOver((GameOver)pro);
                break;
            case INVITE_REQUEST:
                this.handleInviteRequest((InviteRequest)pro);
                break;
            case INVITE_RESULT:
                this.handleInviteResult((InviteResult)pro);
                break;
            case LOGON:
                this.handleLogon((LogonResult)pro);
                break;
            case PUT_PIECE:
                this.handlePutPiece((PutPiece)pro);
                break;
            case QUIT_GAME:
                this.handleQuitGame((QuitGame)pro);
                break;
        }
    }

    /**
     * 放置棋子。
     * @param isBlack   黑棋则为 true，白棋则为 false
     * @param x, y      棋盘坐标
     * @return 成功则返回 true，否则返回 false
     */
    boolean putPiece(boolean isBlack, int x, int y) {
        // 如果超出棋盘区域，则返回 false
        if (x >= Const.GAME_BOARD_POINT_COUNT_H || y >= Const.GAME_BOARD_POINT_COUNT_V) {
            return false;
        }

        // 如果当前点已经有棋子了，则返回 false
        if (mPieces[y][x] != Piece.NOTHING) {
            return false;
        }

        // 画实心圆
        mPaint.setStyle(Paint.Style.FILL);
        if (isBlack) {
            mPaint.setColor(Color.BLACK);
        } else {
            mPaint.setColor(Color.WHITE);
        }
        final float left = (x + 1) * Const.GRID_SIZE - mPieceRadius;
        final float top = (y + 1) * Const.GRID_SIZE - mPieceRadius;
        final float right = left + mPieceRadius * 2;
        final float bottom = top + mPieceRadius * 2;
        final RectF rect = new RectF(left, top, right, bottom);
        mDrawer.drawOval(rect, mPaint);

        // 通知游戏区域重绘
        mGameBoard.invalidate();

        // 保存最后放置棋子的位置
        mLastPos.x = x;
        mLastPos.y = y;

        // 保存到所有棋子中
        if (isBlack) {
            mPieces[y][x] = Piece.BLACK;
        } else {
            mPieces[y][x] = Piece.WHITE;
        }

        return true;
    }

    /**
     * 游戏重新开始。
     */
    public void restart() {
        // 发送挑战请求
        final InviteRequest request = new InviteRequest(mUsername, mRivalName);
        this.sendData(request);
    }

    /**
     * 接收数据的线程方法。
     */
    @Override
    public void run() {
        ObjectInputStream receiver = null;  // 数据接收者
        final Message message = Message.obtain();
        message.what = Const.UM_CONNECTED;

        try {
            // 连接服务器
            final InetAddress serverAddress = InetAddress.getByName(GomokuProtocol.SERVER_IP);
            super.connect(new InetSocketAddress(serverAddress, GomokuProtocol.SERVER_PORT));
        } catch (Exception e) {
            message.obj = false;
            mUICallback.sendMessage(message);
            return;
        }
        message.obj = true;
        mUICallback.sendMessage(message);

        try {
            // 创建数据发送者
            mSender = new ObjectOutputStream(super.getOutputStream());

            // 创建数据接收者
            receiver = new ObjectInputStream(super.getInputStream());

            // 循环接收数据，直到自己被关闭
            while (!super.isClosed()) {
                // 接收数据并转换成协议
                final GomokuProtocol pro = (GomokuProtocol)receiver.readObject();

                // 处理协议
                this.onGetMessageFromServer(pro);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            if (receiver != null) {
                receiver.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 发送数据。
     * @param pro   协议
     */
    public void sendData(GomokuProtocol pro) {
        new SendThread(pro).start();

        // 防止发送过快导致服务器无法响应
        try {
            Thread.sleep(100);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 游戏开始。
     * @param isBlack   黑棋则为 true，白棋则为 false
     */
    public void start(boolean isBlack) {
        // 重绘游戏区域
        this.drawGameBoard();

        // 清空所有棋子
        mPieces = new Piece[Const.GAME_BOARD_POINT_COUNT_V][Const.GAME_BOARD_POINT_COUNT_H];
        for (int i = 0; i < Const.GAME_BOARD_POINT_COUNT_H; i++) {
            for (int j = 0; j < Const.GAME_BOARD_POINT_COUNT_V; j++) {
                mPieces[i][j] = Piece.NOTHING;
            }
        }

        // 设置成员变量
        mStatus = isBlack ? Status.PLAYING: Status.RIVAL_PLAYING;
        mIsBlack = isBlack;

        // 发送通知
        final Message message = Message.obtain();
        message.what = Const.UM_GAME_START;
        message.obj = mRivalName;
        mUICallback.sendMessage(message);
    }
}
